module sw.wiki.wikimodelist;
version(Wiki):
import nazo.http, std.uri, nazo.xhtml, nazo.date, nazo.diff, std.conv, nazo.feed, nazo.string, std.md5;
import sw.mode, sw.contentmode, sw.cgiinfo, sw.siteconfig, sw.wiki.article, sw.trans, sw.locale.locale, sw.unsupportedmode, sw.wiki.pagelist, sw.wiki.recentlist, sw.client, sw.menuitem, sw.commandhelp, sw.forwardmode, sw.wiki.commentlist, sw.name, sw.utils, sw.checker, sw.modelist, sw.page, sw.main;
version(Upload)import sw.upload.uploadmodelist;

/**
 * Wikiモード
 */
class WikiModeFactory:ModeFactory{
  mixin TModeFactory!();
  const static string prefix="wiki";
  IMode getMode(string name){
    if(cfg.synchronizeURI!=""){
      switch(name){
        case "new":
        case "edit":
        case "diff":
        case "backuplist":
        case "backup":
        case "commentrss":
        case "commentrss1.0":
        case "commentrss2.0":
        case "commentrss0.91":
        case "commentatom":
        case "commentatom0.3":
          return new sw.forwardmode.ForwardMode(cfg);
        case "create":
        case "preview":
        case "write":
        case "backupdeleteprevious":
        case "freeze":
        case "unfreeze":
        case "commentwrite":
        case "commentdelete":
          return new UnsupportedMode(cfg);
        default:
      }
    }
    switch(name){
      case "index":
        return new IndexMode(cfg);
      case "new":
        return new NewMode(cfg);
      case "create":
        return new CreateMode(cfg);
      case "list":
        return new ListMode(cfg);
      case "recent":
        return new RecentMode(cfg);
      case "help":
        return new HelpMode(cfg);
      case "rss":
      case "rss1.0":
        return new FeedMode!(FeedType.RSS,1.0)(cfg);
      case "rss2.0":
        return new FeedMode!(FeedType.RSS,2.0)(cfg);
      case "rss0.91":
        return new FeedMode!(FeedType.RSS,0.91)(cfg);
      case "atom":
        return new FeedMode!(FeedType.ATOM)(cfg);
      case "atom0.3":
        return new FeedMode!(FeedType.ATOM,0.3)(cfg);
      case "sitemap":
        return new FeedMode!(FeedType.SITEMAP)(cfg);
      case "show":
      case "":
        return new ShowMode(cfg);
      case "source":
        return new SourceMode(cfg);
      case "edit":
        return new EditMode(cfg);
      case "preview":
        return new PreviewMode(cfg);
      case "write":
        return new WriteMode(cfg);
      case "diff":
        return new DiffMode(cfg);
      case "backuplist":
        return new BackupListMode(cfg);
      case "backup":
        return new BackupMode(cfg);
      case "backupdiff":
        return new BackupDiffMode(cfg);
      case "backupdeleteprevious":
        return new BackupDeletePreviousMode(cfg);
      case "freeze":
        return new FreezeMode(cfg);
      case "unfreeze":
        return new UnfreezeMode(cfg);
      case "commentwrite":
        return new CommentWriteMode(cfg);
      case "commentdelete":
        return new CommentDeleteMode(cfg);
      case "commentshow":
        return new CommentShowMode(cfg);
      case "commentrss":
      case "commentrss1.0":
        return new CommentFeedMode!(FeedType.RSS,1.0)(cfg);
      case "commentrss2.0":
        return new CommentFeedMode!(FeedType.RSS,2.0)(cfg);
      case "commentrss0.91":
        return new CommentFeedMode!(FeedType.RSS,0.91)(cfg);
      case "commentatom":
        return new CommentFeedMode!(FeedType.ATOM)(cfg);
      case "commentatom0.3":
        return new CommentFeedMode!(FeedType.ATOM,0.3)(cfg);
      case "menu":
        return new MenuMode(cfg);
      case "setmenu":
        return new SetMenuMode(cfg);
      default:
    }
    return new UnsupportedMode(cfg);
  }
}

class MenuMode:ForwardMode{
  this(SiteConfig* cfg){
    super(cfg);
  }
  override string title(){
    return "MenuBar";
  }
}

class SetMenuMode:XmlMode!(){
  Article* page;
  mixin sw.contentmode.Page!();
  override d_time lastModified(){
    return page.stateLastModified;
  }
  override string filename(){
    return "menu-xsl.xml";
  }
  override string content(){
    auto menu=page.read();
    return "<?xml version=\"1.0\" encoding=\"utf-8\"?>
<xsl:stylesheet version=\"1.0\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:xh=\"http://www.w3.org/1999/xhtml\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">
"/*"  <xsl:output method=\"xml\" version=\"1.0\" encoding=\"utf-8\" media-type=\"application/xhtml+xml\" doctype-system=\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\" doctype-public=\"-//W3C//DTD XHTML 1.1//EN\"/>"*/ /*hack for MSIE*/ "
  <xsl:template match=\"xh:ul[@class='nl']/xh:li[xh:a[contains(@href,'menu')]]\"/>

  <xsl:template match=\"id('main')\">
    <div class=\"section\" id=\"mainmenu\">
      <h2>メニュー</h2>
      <div class=\"about\">
        "~(canWrite(cfg,page)?"<p id=\"menuedit\"><a href=\""~genURI(cfg,page.title,Http.path)~genURISep(cfg)~"mode=edit\">編集</a></p>
        ":"")~(menu.length?Translator.toXhtml(cfg,menu,3,Http.path):"")~"
      </div>
    </div>
    <xsl:element name=\"{name()}\">
      <xsl:copy-of select=\"@*\"/>
      <xsl:apply-templates/>
    </xsl:element>
  </xsl:template>
  <xsl:template match=\"*\">
    <xsl:element name=\"{name()}\">
      <xsl:copy-of select=\"@*\"/>
      <xsl:apply-templates/>
    </xsl:element>
  </xsl:template>

</xsl:stylesheet>";
  }
}

class CommentShowMode:sw.contentmode.SourceMode{
  this(SiteConfig* cfg){
    super(cfg, CommentList.getFileName(getPageTitle(cfg)));
  }
}

class CommentDeleteMode:XhtmlPageMode!(ModeMethod.POST){
  override string status(){return "(コメントの削除)";}
  bool isSuccess=false;
  string faildReason;
  this(SiteConfig* cfg){
    super(cfg);

    string del;
    string* t="no" in Http.query;
    uint no;
    if(!t||!(*t).length){
      faildReason="コメントナンバーが存在しません。";
      return;
    }
    no=.toUint(*t);
    CommentList cl=new CommentList(page.title);
    if(Client.authority!=Client.Authority.Admin){
      t="del" in Http.query;
      if(!t||!(*t).length){
        faildReason="削除キーが存在しません。";
        return;
      }
      del=decodeComponent((*t).replace("+"," "));
      Comment* cmt=cl.getComment(no);
      ubyte[16] md5;
      sum(md5,del);
      if(cmt.del_md5!=digestToString(md5)){
        faildReason="削除キーが違います。";
        return;
      }
    }
    cl.removeComment(no);
    page.removeCommentLength();
    isSuccess=true;
  }
  override string contentBody(){
    string msg;
    if(!isSuccess)
      msg="削除に失敗しました。("~faildReason~")";
    else
      msg="削除しました。";
    CreateMenuItem cmi=CreateMenuItem(cfg);
    Menu menu;
    cmi.create!("wiki:show")(&menu,"title",page.title);
    return "<p>"~msg~"</p><ul>\n<li>"~menu.content~"</li>\n</ul>";
  }
}

class CommentWriteMode:XhtmlPageMode!(ModeMethod.POST){
  override string status(){return "(コメントの書き込み)";}
  bool isSuccess=false;
  string faildReason;
  this(SiteConfig* cfg){
    super(cfg);

    string del;
    string name;
    string mail;
    string url;
    string* t;
    if(Client.authority!=Client.Authority.Admin){
      t="del" in Http.query;
      if(!t||!(*t).length){
        faildReason="削除キーが存在しません。";
        return;
      }
      del=decodeComponent((*t).replace("+"," "));
      t="name" in Http.query;
      if(!t||!(*t).length){
        faildReason="名前が存在しません。";
        return;
      }
      name=decodeComponent((*t).replace("+"," "));
      if(std.string.find(name,"#")==-1){
        faildReason="トリップが存在しません。";
        return;
      }
      t="mail" in Http.query;
      mail=t?decodeComponent((*t).replace("+"," ")):"";
      if(mail!=""&&std.string.find(mail,"@")==-1){
        faildReason="メールアドレスがおかしいです。";
      }
      t="url" in Http.query;
      url=t?decodeComponent((*t).replace("+"," ")):"";
      if(url!=""&&(url.length<="http://".length||(url[0.."http://".length]!="http://"&&url[0.."https://".length]!="https://"))){
        faildReason="URLがおかしいです。";
        return;
      }
    }else{
      mail=cfg.mailAddr;
      url=Http.relPath;
    }
    name=getNameWithTrip(cfg,name);
    t="message" in Http.query;
    if(!t||!(*t).length){
      faildReason="本文が存在しません。";
      return;
    }
    string message=decodeComponent((*t).replace("+"," "));
    string del_md5;
    ubyte[16] md5;
    sum(md5,del);
    del_md5=digestToString(md5);
    CommentList cl=new CommentList(page.title);
    Comment* cmt=new Comment;
    cmt.newComment(cfg,name,message,url,mail,del_md5);
    cl.addComment(cmt);
    page.addCommentLength();
    isSuccess=true;
  }
  override string contentBody(){
    string msg;
    if(!isSuccess)
      msg="書き込みに失敗しました。("~faildReason~")";
    else
      msg="書き込みました。";
    CreateMenuItem cmi=CreateMenuItem(cfg);
    Menu menu;
    cmi.create!("wiki:show")(&menu,"title",page.title);
    return "<p>"~msg~"</p><ul>\n<li>"~menu.content~"</li>\n</ul>";
  }
}

class CommentFeedMode(FeedType type,double ver=0.0):XmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
    recent=new AllCommentList;
  }
  override string filename(){
    return "comment-feed.xml"; // FIXME: より良い命名を
  }
  override d_time lastModified(){
    return recent.lastModified;
  }
  ///RSSのアイテムの最大個数
  const uint maxlength=15;
  AllCommentList recent;
  override string content(){
    string uri=Http.path();
    scope feed=new Feed!(type,ver)(uri,cfg.siteName,cfg.siteDescription,cfg.defaultLanguage);
    Comment[] cs=recent.getComments();
    for(Comment* li=&cs[0];li<=&cs[$-1];li++){
      feed.addItem(new FeedItem(genURI(cfg, li.page, Http.path),"Re:"~li.page~" - "~li.name,li.datetime,li.message));
    }
    return feed.getContent();
  }
}
class SourceMode:XhtmlPageMode!(){
  override d_time lastModified(){
    return page.stateLastModified;
  }
  this(SiteConfig* cfg){
    super(cfg);
  }
  override string status(){
    return "("~CGI.locale.getValue!(Locale.Page.source)()~")";
  }
  override string contentBody(){
    return "<pre><code>"~Escape.text(page.dat.read())~"</code></pre>";
  }
}

///表示するモード
class ShowMode:EditMode{
  override d_time lastModified(){
    return page.stateLastModified;
  }
  this(SiteConfig* cfg){
    super(cfg);

    if(!page.dat.isExist()){
      if(!cfg.canNewPage&&Client.authority!=Client.Authority.Admin)
        throw new SecurityException;
      isEdit=true;
    }
    if(!isEdit){
      scripts~=Http.basePath~"comment.js";
      footer=`<p id="expanders"><span class="expand" onclick="showHideComment(`~(Client.authority==Client.Authority.Admin?"true":"false")~`)">コメント `~std.string.toString(page.commentLength)~`件</span></p>`;
    }
  }
  override string status(){
    if(isEdit)return (cast(EditMode)super).status();else return "";
  }
  private bool isEdit=false;
  override int permission(){
    if(isEdit)
      return (cast(EditMode)super).permission();
    else
      return Client.Authority.Bot | Client.Authority.Guest |
              Client.Authority.Admin;
  }
  override string contentBody(){
    if(isEdit)return (cast(EditMode)super).contentBody();
    return Translator.toXhtml(cfg,this.page.read(),3);
  }
}

class NewMode:XhtmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override d_time lastModified(){
    return (new CgiInfo).datetime;
  }
  override int permission(){
    return Client.Authority.Guest | Client.Authority.Admin;
  }
  override string title(){return CGI.locale.getValue!(Locale.Page._new)();}
  override string contentBody(){
    if(!cfg.canNewPage&&Client.authority!=Client.Authority.Admin)
      throw new SecurityException;
    Menu[1] menu;
    CreateMenuItem cmi=CreateMenuItem(cfg);
    cmi.createWithId!(Locale.Menu.cancel, cmi.Method.GET)(&menu[0], "wiki:index");
    return "<p>\n<form action=\""~Http.relPath~"\" method=\"get\">\n"
           "<input type=\"hidden\" name=\"mode\" value=\"create\"/>\n"
           "<label>"~CGI.locale.getValue!(Locale.Page.newLabel)()~"<input type=\"text\" name=\"title\"/></label>\n"
           "<input type=\"submit\" value=\"作成\"/>\n"
           ~ menu[0].content ~
           "\n</form>\n</p>";
  }
}

class EditMode:XhtmlPageMode!(){
  override d_time lastModified(){
    return page.stateLastModified;
  }
  this(SiteConfig* cfg){
    super(cfg);
    scripts~=Http.basePath~"edit.js";
  }
  override string status(){
    return "("~CGI.locale.getValue!(Locale.Page.edit)()~")";
  }
  override int permission(){
    return Client.Authority.Guest | Client.Authority.Admin;
  }
  override string contentBody(){
    if(cfg.synchronizeURI.length)throw new Exception("Known Bug: シンクロモードの時、存在しないページをmode=showの状態で飛ばせません。");
    if(!page.writable&&Client.authority!=Client.Authority.Admin)
      throw new SecurityException;
    return "<p>"~CGI.locale.getValue!(Locale.Page.editDescription)()~"</p>\n"~createEditForm(cfg, genURI(cfg, page.title), page.title, page.lastModified, page.read(), page);
  }
}

string createEditForm(SiteConfig* cfg, string path, string title, string lastModified, string data, Article* page){
  string rv;
  CreateMenuItem cmi=CreateMenuItem(cfg);
  Menu[1] menu;

  if(page.dat.isExist())
    cmi.createWithId!(Locale.Menu.cancel, cmi.Method.GET)(&menu[0],"wiki:show","title",title);
  else
    cmi.createWithId!(Locale.Menu.cancel, cmi.Method.GET)(&menu[0],"wiki:index");

  rv~="<p><form action=\""~path~"\" method=\"post\" id=\"edit\">\n<input type=\"hidden\" name=\"title\" value=\""~Escape.arg(title)~"\"/>\n<input type=\"hidden\" name=\"time\" value=\""~lastModified~"\"/>\n<textarea name=\"data\" onfocus=\"escapeEnable();\" onblur=\"escapeDisable(event)\">"~Escape.text(data)~"</textarea>\n<br/>\n";
  if(cfg.wikiDocumentLicense!=0)
    rv~="書き込む場合は必ずページ下記のライセンスに同意してください。<br/>\n";
    rv~="<button name=\"mode\" value=\"preview\" type=\"submit\">プレビュー</button>"
      "<button name=\"mode\" value=\"write\" type=\"submit\">同意して書き込み</button>" ~
      menu[0].content
      ~ "\n</form>\n</p>\n<p><button id=\"escape\" onclick=\"escapeSelectedText();\" disabled=\"disabled\">選択範囲をエスケープ</button></p>";///Bugs: 一部未ローカライズ
  version(Upload)rv~="\n<p><a href=\""~Http.relPath~"?mode="~UploadModeFactory.prefix~":list\">ファイルのアップロード</a></p>";
  return rv;
}

class ForwardMode:Mode!(ModeMethod.GET){
  this(SiteConfig* cfg){
    super(cfg);
  }
  abstract string title();
  override final void response(){
    HttpResponse.write("", 301, ["Location": genURI(cfg, title(), Http.path)]);
  }
}

class IndexMode:ForwardMode{
  this(SiteConfig* cfg){
    super(cfg);
  }
  override string title(){
    return cfg.topPageName;
  }
}

class CreateMode:ForwardMode{
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
    return Client.Authority.Guest | Client.Authority.Admin;
  }
  override string title(){
    if(auto t="title" in Http.query)
      return uridecode(*t);
    return "";
  }
}

class ListMode:XhtmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
    list=new PageList;
  }
  override d_time lastModified(){
    return list.lastModified;
  }
  PageList list;
  override string title(){return CGI.locale.getValue!(Locale.Page.list)();}
  override string contentBody(){
    string content;
    foreach(string li;list.sort){
      assert(li.length);
      content~="<li><a href=\""~genURI(cfg, li)~"\">"~Escape.text(li)~"</a></li>";
    }
    return "<ul>"~content~"</ul>";
  }
}

class RecentMode:XhtmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
    list=new RecentList;
  }
  override d_time lastModified(){
    return list.lastModified;
  }
  RecentList list;
  override string title(){return CGI.locale.getValue!(Locale.Page.recent)();}
  override string contentBody(){
    string content;
    foreach_reverse(string li;list){
      assert(li.length);
      string key=getBefore(li,' ');
      string title=li[key.length+1..$];
      string path=genURI(cfg, title);
      content~="<li>"~time2trim(key)~" <a href=\""~path~"\">"~Escape.text(title)~"</a></li>";
    }
    return "<ul>"~content~"</ul>";
  }
}

class FreezeMode:XhtmlPageMode!(ModeMethod.POST){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
    return Client.Authority.Admin;
  }
  override string status(){return "("~CGI.locale.getValue!(Locale.Page.freeze)()~")";}
  override string contentBody(){
    bool isSuccess=page.writable;
    if(isSuccess)
      page.freeze();
    string msg;
    if(!isSuccess)
      msg=CGI.locale.getValue!(Locale.Page.freezeAlready)();
    else
      msg=CGI.locale.getValue!(Locale.Page.freezeSuccess)();
    CreateMenuItem cmi=CreateMenuItem(cfg);
    Menu[2] menu;
    cmi.create!("wiki:show")(&menu[0],"title",page.title);
    cmi.create!("wiki:edit")(&menu[1],"title",page.title);
    return "<p>"~msg~"</p><ul>\n<li>"~menu[0].content~"</li>\n<li>"~menu[1].content~"</li>\n</ul>";
  }
}

class UnfreezeMode:XhtmlPageMode!(ModeMethod.POST){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override int permission(){
    return Client.Authority.Admin;
  }
  override string status(){return "("~CGI.locale.getValue!(Locale.Page.unfreeze)()~")";}
  override string contentBody(){
    bool isSuccess=!page.writable;
    if(isSuccess)
      page.unfreeze();
    string msg;
    if(!isSuccess)
      msg=CGI.locale.getValue!(Locale.Page.unfreezeYet)();
    else
      msg=CGI.locale.getValue!(Locale.Page.unfreezeSuccess)();
    CreateMenuItem cmi=CreateMenuItem(cfg);
    Menu[2] menu;
    cmi.create!("wiki:show")(&menu[0],"title",page.title);
    cmi.create!("wiki:edit")(&menu[1],"title",page.title);
    return "<p>"~msg~"</p><ul>\n<li>"~menu[0].content~"</li>\n<li>"~menu[1].content~"</li>\n</ul>";
  }
}

import std.regexp;
template TimeMode(){
  static string time;
  void timeModeInit(){
    if(auto t="time" in Http.query)
      this.time=*t;
  }
}

class BackupMode:XhtmlPageMode!(){
  mixin TimeMode;
  this(SiteConfig* cfg){
    super(cfg);
    timeModeInit();
  }
  override string status(){
    try{
      return "("~CGI.locale.getValue!(Locale.Page.backup)().replace("%s",time2trim(time))~")";
    }catch(Exception e){
      throw new InvalidArgsException("time");
    }
  }
  override string contentBody(){
    return "<pre><code>"~Escape.text(page.backupRead(time))~"</code></pre>";
  }
}

string diffHTML(string a, string b){
  Diff mydiff=new Diff(a, b);
  string r;
  foreach(line; mydiff.getDiff()){
    r ~= line[0] == '+' ? "<span class=\"diff-add-line\">" :
         line[0] == '-' ? "<span class=\"diff-remove-line\">" : "";
    r ~= Escape.text(line);
    if(line[0]!=' ')
      r ~= "</span>";
  }
  return r;
}

class DiffMode:XhtmlPageMode!(){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override string status(){return "("~CGI.locale.getValue!(Locale.Page.diff)()~")";}
  override string contentBody(){
    if(page.lastBackup==""){
      return "<p>"~CGI.locale.getValue!(Locale.Page.diffNodiff)()~"</p>";
    }else{
      auto r=diffHTML(page.backupRead(page.lastBackup),page.backupRead(page.lastModified));

      return "<pre><code>"~r~"</code></pre>";
    }
  }
}

class BackupDiffMode:XhtmlPageMode!(){
  mixin TimeMode;
  this(SiteConfig* cfg){
    super(cfg);
    timeModeInit();
  }
  override string status(){return "("~CGI.locale.getValue!(Locale.Page.backupdiff)().replace("%s",time2trim(time))~")";}
  override string contentBody(){
    auto r=diffHTML(page.backupRead(time),page.backupRead(page.lastModified));
    return "<pre><code>"~r~"</code></pre>";
  }
}

class BackupListMode:XhtmlPageMode!(){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override string status(){return "("~CGI.locale.getValue!(Locale.Page.backuplist)()~")";}
  override string contentBody(){
    string content;
    bool first=true;
    foreach_reverse(string li;page.backupList){
      assert(li.length);
      if(first){
        first=false;
        continue;
      }
      string path=genURI(cfg, page.title);
      string sep=genURISep(cfg);
      content~="<li><a href=\""~path~sep~"mode=backup&amp;time="~li~"\">"~time2trim(li)~"</a> (<a href=\""~path~sep~"mode=backupdiff&amp;time="~li~"\">"~CGI.locale.getValue!(Locale.Page.backuplistDiff)()~"</a>"~(Client.authority==Client.Authority.Admin?",<form action=\""~path~"\" method=\"post\" style=\"display:inline\">\n<input type=\"hidden\" name=\"title\" value=\""~Escape.text(page.title)~"\"/>\n<input type=\"hidden\" name=\"mode\" value=\"backupdeleteprevious\"/>\n<input type=\"hidden\" name=\"time\" value=\""~li~"\"/>\n<input type=\"submit\" value=\""~CGI.locale.getValue!(Locale.Page.backuplistDeleteprevious)()~"\"/>\n</form>":"")~")</li>";
    }
    return "<ul>"~content~"</ul>";
  }
}

///Bugs: 未ローカライズ
class BackupDeletePreviousMode:XhtmlPageMode!(ModeMethod.POST){
  mixin TimeMode;
  this(SiteConfig* cfg){
    super(cfg);
    timeModeInit();
  }
  override int permission(){
    return Client.Authority.Admin;
  }
  override string status(){return "バックアップの削除";}
  override string contentBody(){
    foreach(string li;page.backupList){
      assert(li.length);
      if(.toInt(li)>.toInt(time))break;
      page.backupDelete(li);
    }
    CreateMenuItem cmi=CreateMenuItem(cfg);
    Menu[2] menu;
    cmi.create!("wiki:show")(&menu[0],"title",page.title);
    cmi.create!("wiki:backuplist")(&menu[1],"title",page.title);
    return "<p>バックアップを削除しました</p><ul>\n<li>"~menu[0].content~"</li>\n<li>"~menu[1].content~"</li>\n</ul>";
  }
}

template WithSourceMode(){
  static string data;
  void withSourceModeInit(){
    if(auto t="data" in Http.query)
      data=decodeComponent((*t).replace("+"," "));
  }
}

class WriteMode:XhtmlPageMode!(ModeMethod.POST){
  mixin TimeMode;
  mixin WithSourceMode;
  private string _status;
  private string _body;
  override string status(){return _status;}
  override string contentBody(){return _body;}
  this(SiteConfig* cfg){
    super(cfg);

    timeModeInit();
    withSourceModeInit();
    if(!page.writable&&Client.authority!=Client.Authority.Admin)
      throw new SecurityException;
    if(!cfg.canNewPage&&Client.authority!=Client.Authority.Admin&&page.lastModified=="0")
      throw new SecurityException;

    auto err = check(data);
    if(err){
      _body = "<p>エラー: "~std.string.toString(err.line)~"文字目 - "~Escape.text(err.msg)~"</p>";
      _body~=createEditForm(cfg, genURI(cfg, page.title), page.title, time, data, page);
      return;
    }
    auto pagedata=page.read();
    if(data==pagedata){
      CreateMenuItem cmi=CreateMenuItem(cfg);
      Menu[2] menu;
      cmi.create!("wiki:show")(&menu[0],"title",page.title);
      cmi.create!("wiki:edit")(&menu[1],"title",page.title);
      _body="<p>変更されてませんでした。</p><ul>\n<li>"~menu[0].content~"</li>\n<li>"~menu[1].content~"</li>\n</ul>";
      return;
    }
    if(time!=page.lastModified){
      auto r = diffHTML(page.backupRead(page.lastModified),data);
      _status="("~CGI.locale.getValue!(Locale.Page.conflict)()~")";
      _body="<p>"~CGI.locale.getValue!(Locale.Page.conflictDescription)~"</p><h3>"~CGI.locale.getValue!(Locale.Page.conflictDiff)()~"</h3><pre><code>"~r~"</code></pre>" ~ createEditForm(cfg, genURI(cfg, page.title), page.title, page.lastModified, pagedata, page);
      return;
    }
    page.write(data);
    CreateMenuItem cmi=CreateMenuItem(cfg);
    Menu[2] menu;
    if(data.length){
      cmi.create!("wiki:show")(&menu[0],"title",page.title);
      cmi.create!("wiki:edit")(&menu[1],"title",page.title);
      _status="("~CGI.locale.getValue!(Locale.Page.write)()~")";
      _body="<ul>\n<li>"~menu[0].content~"</li>\n<li>"~menu[1].content~"</li>\n</ul>";
    }else{
      cmi.create!("wiki:index")(&menu[0]);
      cmi.createWithId!(Locale.Menu.reCreate, cmi.Method.GET)(&menu[1],"wiki:edit","title",page.title);
      _status="("~CGI.locale.getValue!(Locale.Page._delete)()~")";
      _body="<ul>\n<li>"~menu[0].content~"</li>\n<li>"~menu[1].content~"</li>\n</ul>";
    }
  }
}

class PreviewMode:XhtmlPageMode!(ModeMethod.POST){
  mixin TimeMode;
  mixin WithSourceMode;
  this(SiteConfig* cfg){
    super(cfg);
    timeModeInit();
    withSourceModeInit();
  }
  override string status(){return "("~CGI.locale.getValue!(Locale.Page.preview)()~")";}
  override string contentBody(){
    string _body;
    if(!page.writable&&Client.authority!=Client.Authority.Admin)
      throw new SecurityException;
    scripts~=Http.basePath~"edit.js";
    auto err = check(data);
    if(!err)
      _body = Translator.toXhtml(cfg,data,4);
    else
      _body = "<p>エラー: "~std.string.toString(err.line)~"文字目 - "~Escape.text(err.msg)~"</p>";
    _body~=createEditForm(cfg, genURI(cfg, page.title), page.title, time, data, page);
    return _body;
  }
}

///Bugs: 未ローカライズ
class HelpMode:XhtmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
  }
  override d_time lastModified(){
    return (new CgiInfo).datetime;
  }
  override string title(){return CGI.locale.getValue!(Locale.Page.help)();}
  override string contentBody(){
    string result;
    Section sec;
    result~=(sec.initialize("Wikiって何?","<p>誰でも自由に編集できるシステムです。</p>",2)).getContents();
    result~=(sec.initialize("構文","<p>構文は[名前 パラメータ1,パラメータ2,パラメータ3...]という形で、動作は関数のオーバーライドの様な感じです。<br/>\n名前、パラメータには改行が含められます。<br/>\n\"[\"、\"]\"、\",\"のエスケープは\\で行います。\\自体は\\\\で入力可能です。</p>",2)).getContents();
    result~=(sec.initialize("対応文",getHelp(),2)).getContents();
    return result;
  }  
}

class FeedMode(FeedType type,double ver=0.0):XmlMode!(){
  this(SiteConfig* cfg){
    super(cfg);
    recent=new RecentList;
  }
  override string filename(){
    return "feed.xml"; // FIXME: より良い命名を
  }
  override d_time lastModified(){
    return recent.lastModified;
  }
  ///RSSのアイテムの最大個数
  const uint maxlength=15;
  RecentList recent;
  override string content(){
    string uri=Http.path();
    scope feed=new Feed!(type,ver)(uri,cfg.siteName,cfg.siteDescription,cfg.defaultLanguage);
    uint i=0;
    foreach_reverse(string li;recent){
      if(i++>=this.maxlength)break;
      assert(li.length);
      string key=getBefore(li,' ');
      string title=li[key.length+1..$];
      feed.addItem(new FeedItem(genURI(cfg,title,Http.path),title,getUTCtimeFromUtime(.toUint(key))));
    }
    return feed.getContent();
  }
}

